﻿//****************************************************************************
// Copyright (c) Microsoft Corporation. All rights reserved.
//****************************************************************************
using System;
using System.IO;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Web.Configuration;
using System.Reflection;
using System.Globalization;
using System.Xml;

using Microsoft.FxCop.Sdk;

namespace Microsoft.FxCop.Rules.ASP.Security.Configuration
{
    public abstract class WebConfigurationIntrospectionRule : ConfigurationIntrospectionRule
    {
        #region *** Fields ***
        private static Type s_fxcopOMType;
        private static Type s_fxcopProjectType;
        private static Dictionary<string, HashSet<string>> s_suppressions;
        private static Hashtable s_checkCompleted = new Hashtable();
        private static bool? s_aspAnalysis = null;
        private static Object s_initLock = new Object();
        #endregion

        #region *** Constructor ***
        protected WebConfigurationIntrospectionRule(string name) :
            base(name, "Microsoft.FxCop.Rules.ASP.Security.Configuration.AspNetConfigurationSecurityRules", typeof(WebConfigurationIntrospectionRule).Assembly) { }
        #endregion

        #region *** Methods ***

        #region *** Overrides ***
        public override void BeforeAnalysis()
        {
            base.BeforeAnalysis();

            lock (s_initLock)
            {

                if ((s_fxcopOMType == null) || (s_fxcopProjectType == null))
                    InitFxCopTypes();

                // We can't use static constructor here because it will get called before the FxCopOM is populated.
                if (s_aspAnalysis == null)
                {
                    s_aspAnalysis = IsAspModeEnabled();
                }

                if (s_aspAnalysis.Value)
                {
                    // read suppressions
                    if (s_suppressions == null)
                        CollectSuppressions();
                }
            }
        }

        public override ProblemCollection Check(ModuleNode module)
        {
            System.Configuration.Configuration[] configFiles;
            string rootAppFolder;

            // if we are not doing /asp analysis, we skip
            if (!s_aspAnalysis.Value)
                return null;

            rootAppFolder = GetWebRootFolder(module);
            configFiles = ConfigurationProbing(rootAppFolder);

            foreach (System.Configuration.Configuration config in configFiles)
            {
                if (ShouldRunRule(config.FilePath, this.CheckId))
                {
                    Check(config);
                }
            }

            return base.Problems;
        }
        #endregion

        #region *** Protected ***
        protected static System.Configuration.Configuration[] ConfigurationProbing(string rootAppFolder)
        {
            string[] configFiles;
            List<System.Configuration.Configuration> webConfigFiles;
            System.Configuration.Configuration webConfigFile;

            webConfigFiles = new List<System.Configuration.Configuration>();

            configFiles = Directory.GetFiles(rootAppFolder, "web.config", SearchOption.AllDirectories);

            foreach (string configFile in configFiles)
            {
                webConfigFile = CreateWebConfiguration(rootAppFolder, configFile);
                webConfigFiles.Add(webConfigFile);
            }

            return webConfigFiles.ToArray();
        }
        #endregion

        #region *** Private ***
        private static bool IsCheckSuppressed(string configFile, string checkid)
        {
            if (s_suppressions.ContainsKey(configFile))
            {
                return s_suppressions[configFile].Contains(checkid);
            }

            return false;
        }

        private static void CollectSuppressions()
        {
            HashSet<string> suppressedChecks;
            string checkid;
            string configFile;

            s_suppressions = new Dictionary<string, HashSet<string>>();

            foreach (AssemblyNode assembly in RuleUtilities.AnalysisAssemblies)
            {
                foreach (AttributeNode attrNode in assembly.Attributes)
                {
                    if (attrNode.Type == FrameworkTypes.SuppressMessageAttribute)
                    {

                        // Suppression example:
                        // SuppressMessage("ASP.NET.Security.Configuration","CA5306:FormAuthenticationRequireSSLShouldBeTrue", Target="/web.config")]

                        // we need to extract the checkid. Base on SuppressMessageAttribute,
                        // the checkid will always be the second parameter
                        // we extract by retreiving the second Expression
                        checkid = GetCheckId(attrNode);

                        configFile = GetTargetNamedParameter(attrNode);

                        // If no Target is specified or if the Target is an empty string, we consider the suppression
                        // to be for all config file
                        if (string.IsNullOrEmpty(configFile))
                            configFile = "*";

                        if (!s_suppressions.ContainsKey(configFile))
                        {
                            suppressedChecks = new HashSet<string>();
                            suppressedChecks.Add(checkid);
                            s_suppressions.Add(configFile, suppressedChecks);
                        }
                        else
                        {
                            suppressedChecks = s_suppressions[configFile];
                            if (!suppressedChecks.Contains(checkid))
                                suppressedChecks.Add(checkid);
                        }
                    }
                }
            }
        }

        private static string GetCheckId(AttributeNode attrNode)
        {
            string id;
            int pos;

            id = attrNode.Expressions[1].ToString();

            pos = id.IndexOf(':');

            if (pos < 0)    // if : is not in the id, they didn't specify rule name
                return id;
            else
                return id.Substring(0, pos);
        }

        private static string GetTargetNamedParameter(AttributeNode attrNode)
        {
            NamedArgument namedArg;

            if (attrNode.Expressions.Count > 2)
            {
                for (int i = 2; i < attrNode.Expressions.Count; i++)
                {
                    if (attrNode.Expressions[i] is Microsoft.FxCop.Sdk.NamedArgument)
                    {
                        namedArg = (NamedArgument)attrNode.Expressions[i];

                        if (namedArg.Name.Name == "Target")
                        {
                            return namedArg.Value.ToString();
                        }
                    }
                }
            }

            return null;
        }

        private static System.Configuration.Configuration CreateWebConfiguration(string rootAppFolder, string webConfigFile)
        {
            WebConfigurationFileMap configMap;
            string webConfigFileFolder;

            webConfigFileFolder = Path.GetDirectoryName(webConfigFile);

            configMap = CreateFileMap(rootAppFolder, webConfigFileFolder);
            return WebConfigurationManager.OpenMappedWebConfiguration(configMap, "/targetconfig");
        }

        private static WebConfigurationFileMap CreateFileMap(string rootSiteFolder, string targetAppFolder)
        {
            WebConfigurationFileMap fileMap;
            VirtualDirectoryMapping vDirMap;
            VirtualDirectoryMapping vDirMapBase;

            fileMap = new WebConfigurationFileMap();
            vDirMap = new VirtualDirectoryMapping(targetAppFolder, true);
            fileMap.VirtualDirectories.Add("/targetconfig", vDirMap);

            vDirMapBase = new VirtualDirectoryMapping(rootSiteFolder, true, "web.config");

            // Add it to the virtual directory mapping collection.
            fileMap.VirtualDirectories.Add("/", vDirMapBase);

            // Return the mapping.
            return fileMap;
        }

        private static string GetWebRootFolder(ModuleNode moduleNode)
        {
            string webRootFolder;

            if( (webRootFolder = GetWebRootFolder_EnvironmentVariable()) == null )
            {
                if ((webRootFolder = GetWebRootFolder_DefaultPath(moduleNode)) == null)
                {
                    throw (new FileNotFoundException(Localized.NoWebConfigFileFound));
                }
            }

            return webRootFolder;
        }

        private static string GetWebRootFolder_EnvironmentVariable()
        {
            return Environment.GetEnvironmentVariable("TargetWebApplicationRootFolder");
        }

        private static string GetWebRootFolder_DefaultPath(ModuleNode moduleNode)
        {
            string rootPath;
            string probeConfig;

            rootPath = Path.GetDirectoryName(moduleNode.Location) + @"\";
            probeConfig = Path.GetFullPath( Path.Combine(rootPath, @"..\web.config") );

            if (!File.Exists(probeConfig))
                return null;

            return Path.GetDirectoryName(probeConfig);
        }

        /// <summary>
        /// Verifies if the rule is suppressed or if we already ran the rule on a give config file.
        /// This eliminate running the same rule on the same configFile. This can occur when fxcop is running
        /// on multiple assemblies App*.dll in a given web project
        /// </summary>
        /// <param name="configFile"></param>
        /// <param name="checkId"></param>
        /// <returns>Returns true if the rule didn't run on the configFile yet. False if we already ran the rule on the configFile</returns>
        private static bool ShouldRunRule(string configFile, string checkId)
        {
            HashSet<string> checks;

            // we first verify if the check is suppressed in the configuration file
            if (IsCheckSuppressed(configFile, checkId))
                return false;

            // we check to see if the rule already ran
            lock (s_checkCompleted)
            {
                if (!s_checkCompleted.Contains(configFile))
                {
                    checks = new HashSet<string>();
                    checks.Add(checkId);
                    s_checkCompleted.Add(configFile, checks);

                    return true;
                }
                else
                {
                    checks = (HashSet<string>)s_checkCompleted[configFile];
                    if (!checks.Contains(checkId))
                    {
                        checks.Add(checkId);
                        return true;
                    }
                    else
                    {
                        return false;
                    }
                }
            }
        }


        #region ASP Scan Check
        private static bool IsAspModeEnabled()
        {
            bool enabled = false;

            if (IsRunningFxcopCmd())
            {

                enabled = (bool)s_fxcopOMType.InvokeMember("get_AnalyzeOnlyAspNetCode",
                                                            BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
                                                            null,
                                                            null,
                                                            null,
                                                            CultureInfo.InvariantCulture);

                if (!enabled)
                    return IsProjectEnableAspScan();
                else
                    return enabled;
            }
            else
            {
                return IsProjectEnableAspScan();
            }
        }

        private static void InitFxCopTypes()
        {
            Type[] commonTypes;

            commonTypes = typeof(Microsoft.FxCop.Common.FxCopCategoryInfo).Assembly.GetTypes();

            foreach (Type t in commonTypes)
            {
                if (t.Name == "FxCopOM")
                {
                    s_fxcopOMType = t;
                }

                if (t.Name == "Project")
                {
                    s_fxcopProjectType = t;
                }

                if ((s_fxcopOMType != null) && (s_fxcopProjectType != null))
                    break;
            }

            if (s_fxcopOMType == null)
                throw (new TypeNotFoundException("FxCopOM type was not found"));
            if (s_fxcopProjectType == null)
                throw (new TypeNotFoundException("Project type was not found"));
        }

        private static bool IsRunningFxcopCmd()
        {
            return (bool)s_fxcopOMType.InvokeMember("get_IsFxCopCmd",
                                                    BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
                                                    null,
                                                    null,
                                                    null,
                                                    CultureInfo.InvariantCulture);
        }

        private static string GetProjectFile()
        {
            object project;

            project = s_fxcopOMType.InvokeMember("get_Project",
                                                 BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
                                                 null,
                                                 null,
                                                 null,
                                                 CultureInfo.InvariantCulture);

            return (string)s_fxcopProjectType.InvokeMember("get_FileName",
                                                            BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance,
                                                            null,
                                                            project,
                                                            null,
                                                            CultureInfo.InvariantCulture);

        }

        private static bool IsProjectEnableAspScan()
        {
            XmlDocument xmlDoc;
            XmlNode xmlNode;
            bool enabled;
            string projectFile;

            projectFile = GetProjectFile();

            // No project used
            if (string.IsNullOrEmpty(projectFile))
                return false;

            xmlDoc = new XmlDocument();

            xmlDoc.Load(projectFile);

            if ((xmlNode = xmlDoc.SelectSingleNode("/FxCopProject/ProjectOptions/ScanAsp")) == null)
                return false;

            if (bool.TryParse(xmlNode.InnerText, out enabled))
            {
                return enabled;
            }
            else
            {
                throw (new ArgumentException("ScanAsp should be True or False"));
            }
        }
        #endregion

        #endregion

        #endregion
    }
}
